var root = this;

var $ = root.jQuery || root.Zepto || root.ender || root.$;

var $_id = 1,
    $_tracer = [],
    $_doTrace = function (obj) {
        var i;
        for (i = 0; i < $_tracer.length; i++) {
            setTimeout(function (f, o) {
                return function () {
                    f(o);
                }
            }($_tracer[i], obj), 0);
        }
    },

$_jsonRPC = function (service, method, args, predicates, cacheDuration) {
    var doTrace = $_doTrace,
        cacheKey = [service, method].join('.'),
        requestid = $_id,
        d = new $.Deferred(),
        request = { jsonrpc: "2.0", "method": method, "params": args, "id": requestid },
        cachedvalue,
        validator;

    // For backward comp.
    d.complete = d.done;
    d.error = d.fail;

    doTrace(request);
    if ($_cache.contains(cacheKey, args, cacheDuration)) {
        cachedvalue = $_cache.get(cacheKey, args);
        doTrace({ "jsonrpc": "2.0", "id": requestid, result: cachedvalue });
        d.resolve(cachedvalue);
    } else {
        validator = new Validator(predicates);
        if (!validator.validate(args)) {
            doTrace({ "jsonrpc": "2.0", "id": requestid, error: (validator.getMessages() || "Validation error") });
            d.reject(validator.getMessages());
        } else {
            $.post(service, JSON.stringify(request))
                .success(function (r) {
                    var cache = $_cache, duration = cacheDuration;
                    try {
                        if (r.error) {
                            doTrace(r);
                            d.reject(r.error);
                        } else {
                            if (duration) {
                                cache.set(cacheKey, args, resp);
                            }
                            doTrace(r);
                            d.resolve(r.result);
                        }
                    }
                    catch (err) {
                        var response = { "jsonrpc": "2.0", "id": requestid, error: err };
                        doTrace(response);
                        d.reject(response.error);
                    }
                })
            .fail(function (e) {
                var response = { "jsonrpc": "2.0", "id": requestid, error: e };
                doTrace(response);
                d.reject(response.error);
            });
        }
    }
    return d;
},

$_cache = (function () {
    var $_cache = {};
    function getKey(method, args) {
        return method + args.toString();
    }
    return {
        contains: function (method, args, duration) {
            var key = getKey(method, args);
            return $_cache.hasOwnProperty(key) && $_cache[key].cached.getTime() + (duration * 1000) > (new Date()).getTime();
        }, get: function (method, args) {
            var key = getKey(method, args);
            if (!$_cache.hasOwnProperty(key)) {
                return null;
            }
            return $_cache[key].result;
        }, set: function (method, args, r) {
            $_cache[getKey(method, args)] = { result: r.result, cached: new Date() };
        }
    };
})(),

$_toTypeString = function (obj) {
    return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
},

Service = {
    rpc: $_jsonRPC
};

function Validator(predicates) {
    var messages;
    return {
        validate: function (args) {
            var i;
            messages = [];
            for (i = 0; i < predicates.length; i++) {
                predicates[i] && predicates[i](args[i], messages);
            }
            return messages.length == 0;
        }, getMessages: function (callback) {
            var i;
            if (typeof callback == 'function') {
                for (i = 0; i < messages.length; i++) {
                    callback(messages[i]);
                }
                return undefined;
            } else {
                return messages.join(', ');
            }
        }
    };
}

function Muccu() {
}

Muccu.prototype.trace = function (callback) {
    if (typeof callback === 'function')
        $_tracer.push(callback);
    return this;
};
Muccu.prototype.debug = function () {
    this.trace(function (r) {
        if (r.error)
            console && console.warn(r.error);
        else if (r.result)
            console && console.dir(r.result);
        else
            console && console.dir(r);
    });
};